package me.zhengjie.modules.partner.service.impl;

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import me.zhengjie.exception.BadRequestException;
import me.zhengjie.modules.partner.service.PartnerService;
import me.zhengjie.modules.partner.service.TokenService;
import me.zhengjie.modules.partner.service.dto.PartnerDto;
import me.zhengjie.modules.partner.service.dto.TokenDto;
import me.zhengjie.utils.RedisUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.crypto.SecretKey;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @className TokenServiceImpl
 * @description Token实现类
 * @author jianyongjie@126.com
 * @date 2021/8/3 3:07 下午
 * @version 1.0.0
 **/
@Service
@Slf4j
public class TokenServiceImpl implements TokenService {
    /** Token 请求获取的次数 key */
    private static final String PREFIX_TOKEN_COUNT_KEY = "token:count:";
    /** Token 存储Redis中的 key */
    private static final String PREFIX_TOKEN_CACHE_KEY = "token:cache:";
    /** Token 获取分布式锁 key */
    private static final String PREFIX_TOKEN_LOCK_KEY = "token:lock:";
    /** Token 当天23:59:59秒 最大获取次数 key */
    private static final int MAX_TOKEN_COUNT = 10000;
    /** 加锁 默认过期时间 秒*/
    private static final int LOCK_EXPIRE_SECONDS = 5;
    /** 15天的 毫秒*/
    private static final long TOKEN_EXPIRE_SECONDS = 15 * 24 * 60 * 60 * 1000;

    @Resource
    private PartnerService partnerService;

    @Resource
    private RedisUtils redisUtils;

    /**
     * 生成Token信息
     * @param appId
     * @param appSecret
     * @return
     */
    @Override
    public TokenDto createToken(Long appId, String appSecret) {
        final PartnerDto partnerDto = partnerService.findById(appId);
        if (!partnerDto.getEnable()) {
            throw new BadRequestException("商户已禁用，无法获取Token");
        }

        if (!StrUtil.equals(partnerDto.getAppSecret(), appSecret)) {
            throw new BadRequestException("获取Token秘钥验证失败，请检查秘钥信息");
        }

        try {
            if (redisUtils.lock(PREFIX_TOKEN_LOCK_KEY + appId, LOCK_EXPIRE_SECONDS)) {
                log.info("appId:{}获取到锁资源，开始生成Token信息", appId);

                DateTime dayLastTime = DateUtil.endOfDay(new Date());
                final Long incrementAndGetAt = redisUtils.incrementAndGetAt(PREFIX_TOKEN_COUNT_KEY + appId, new Date(dayLastTime.getTime()));
                if (incrementAndGetAt > MAX_TOKEN_COUNT) {
                    throw new BadRequestException("当天Token只允许获取10次");
                }

                log.info("appId:{} token获取数量验证通过，当前次数为:{}", appId, incrementAndGetAt);
                Date expireDate = new Date(System.currentTimeMillis() + TOKEN_EXPIRE_SECONDS);
                final String jwtToken = createJWT(appSecret, partnerDto, expireDate);
                log.info("appId:{} 第{}次生成的Token为:{},过期时间为:{}", appId, incrementAndGetAt, jwtToken, DateUtil.formatDateTime(expireDate));

                //保存到Redis中
                redisUtils.set(PREFIX_TOKEN_CACHE_KEY + appId, jwtToken, expireDate.getTime(), TimeUnit.MILLISECONDS);

                return TokenDto.builder().token(jwtToken).expireTime(expireDate.getTime()).build();
            }
            throw new BadRequestException("资源正忙，请稍后重试");
        } finally {
            redisUtils.unlock(PREFIX_TOKEN_LOCK_KEY + appId);
        }
    }

    @Override
    public String verifyToken(Long appId, String token) {
        final PartnerDto partnerDto = partnerService.findById(appId);

        final String cacheToken = (String) redisUtils.get(PREFIX_TOKEN_CACHE_KEY + appId);
        if (StrUtil.isEmpty(cacheToken) || !Objects.equals(cacheToken, token)) {
            log.warn("Redis中Token验证失败，appId:{} Token:{}，Redis中Token:{}", appId, token, cacheToken);
            throw new BadRequestException("Token验证失败，缓存中不匹配或已失效");
        }

        if (verifyJWT(appId, token)) {
            return parseJWT(partnerDto.getAppSecret(), token).getSubject();
        }
        throw new BadRequestException("Token验证失败");
    }

    /**
     * 签发JWT，创建token的方法
     * @param key 秘钥Key
     * @param expiration 过期时间
     * @return 返回Token信息
     */
    private static String createJWT(String key, PartnerDto dto, Date expiration) {
        try {
            Map<String, Object> claims = new HashMap<>();
            claims.put("id", dto.getId());
            claims.put("name", dto.getName());
            claims.put("phone", dto.getPhone());

            SecretKey secretKey = Keys.hmacShaKeyFor(key.getBytes(Charset.forName("UTF-8")));
            JwtBuilder builder = Jwts.builder()
                    .setId(createJti())
                    .setClaims(claims)
                    .setSubject(String.valueOf(dto.getId()))
                    .setIssuedAt(new Date())
                    .setExpiration(expiration)
                    //设置密匙和算法
                    .signWith(secretKey);
            return builder.compact();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static String createJti() {
        return StringUtils.replace(UUID.randomUUID().toString(), "-", "");
    }

    private boolean verifyJWT(Long appId, String token) {
        log.info("appId:{}验证Token:{}", appId, token);
        final PartnerDto partnerDto = partnerService.findById(appId);
        try {
            parseJWT(partnerDto.getAppSecret(), token);
            return Boolean.TRUE;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return Boolean.FALSE;
        }
    }

    private Claims parseJWT(String key, String token) {
        final JwtParser build = Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(key.getBytes(Charset.forName("UTF-8")))).build();
        return build.parseClaimsJws(token).getBody();
    }
}
